package com.dek.grpc.errorhandle;

import com.dek.grpc.dto.HelloReply;
import com.dek.grpc.dto.HelloRequest;
import com.dek.grpc.service.HelloServiceGrpc;
import com.google.common.base.Verify;
import com.google.common.base.VerifyException;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.rpc.DebugInfo;
import io.grpc.*;
import io.grpc.protobuf.ProtoUtils;
import io.grpc.stub.StreamObserver;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import static com.google.common.util.concurrent.MoreExecutors.directExecutor;

public class DetailErrorSample {

    private static final Metadata.Key<DebugInfo> DEBUG_INFO_KEY = ProtoUtils.keyForProto(DebugInfo.getDefaultInstance());

    private static final DebugInfo DEBUG_INFO = DebugInfo.newBuilder().addStackEntries("entry_1")
                                                                        .addStackEntries("entry_2")
                                                                        .addStackEntries("entry_3")
                                                                    .setDetail("detail error info").build();

    private static final String DEBUG_DESC = "detailed error description";

    public static void main(String[] args) throws IOException, InterruptedException {
        new DetailErrorSample().run();
    }

    private ManagedChannel channel;

    private void run() throws IOException, InterruptedException {
        Server server = ServerBuilder.forPort(0).addService(new HelloServiceGrpc.HelloServiceImplBase() {
            @Override
            public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
                Metadata entrys = new Metadata();
                entrys.put(DEBUG_INFO_KEY, DEBUG_INFO);
                responseObserver.onError(Status.INTERNAL.withDescription(DEBUG_DESC).asRuntimeException(entrys));
            }
        }).build().start();
        channel = ManagedChannelBuilder.forAddress("localhost", server.getPort()).usePlaintext().build();
        blockingCall();
        futureCallDirect();
        futureCallCallback();
        asyncCall();
        advancedAsyncCall();

        channel.shutdown();
        server.shutdown();
        channel.awaitTermination(1, TimeUnit.SECONDS);
        server.awaitTermination();

    }

    void blockingCall() {
        HelloServiceGrpc.HelloServiceBlockingStub stub = HelloServiceGrpc.newBlockingStub(channel);
        try {
            stub.sayHello(HelloRequest.newBuilder().build());
        } catch (Exception e) {
            verifyErrorReply(e);
        }
    }

    void futureCallDirect() {
        HelloServiceGrpc.HelloServiceFutureStub stub = HelloServiceGrpc.newFutureStub(channel);
        ListenableFuture<HelloReply> response = stub.sayHello(HelloRequest.newBuilder().build());
        try {
            response.get();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
            verifyErrorReply(e.getCause());
        }
    }

    void futureCallCallback() {
        HelloServiceGrpc.HelloServiceFutureStub stub = HelloServiceGrpc.newFutureStub(channel);
        ListenableFuture<HelloReply> response = stub.sayHello(HelloRequest.newBuilder().build());
        final CountDownLatch latch = new CountDownLatch(1);
        Futures.addCallback(response, new FutureCallback<HelloReply>() {
            @Override
            public void onSuccess(@NullableDecl HelloReply helloReply) {

            }

            @Override
            public void onFailure(Throwable throwable) {
                verifyErrorReply(throwable);
                latch.countDown();
            }
        }, directExecutor());
        if (!Uninterruptibles.awaitUninterruptibly(latch, 1, TimeUnit.SECONDS)) {
            throw new RuntimeException("timeout!");
        }
    }

    void asyncCall() {
        HelloServiceGrpc.HelloServiceStub stub = HelloServiceGrpc.newStub(channel);
        HelloRequest request = HelloRequest.newBuilder().build();
        final CountDownLatch latch = new CountDownLatch(1);
        StreamObserver<HelloReply> responseObserver = new StreamObserver<HelloReply>() {
            @Override
            public void onNext(HelloReply helloReply) {

            }

            @Override
            public void onError(Throwable throwable) {
                verifyErrorReply(throwable);
                latch.countDown();
            }

            @Override
            public void onCompleted() {

            }
        };
        stub.sayHello(request, responseObserver);
        if (!Uninterruptibles.awaitUninterruptibly(latch, 1, TimeUnit.SECONDS)) {
            throw new RuntimeException("timeout!");
        }

    }

    void advancedAsyncCall() {
        ClientCall<HelloRequest, HelloReply> call = channel.newCall(HelloServiceGrpc.getSayHelloMethod(), CallOptions.DEFAULT);

        final CountDownLatch latch = new CountDownLatch(1);

        call.start(new ClientCall.Listener<HelloReply>() {
            @Override
            public void onClose(Status status, Metadata trailers) {
                Verify.verify(status.getCode() == Status.Code.INTERNAL);
                Verify.verify(trailers.containsKey(DEBUG_INFO_KEY));
                try {
                    Verify.verify(trailers.get(DEBUG_INFO_KEY).equals(DEBUG_INFO));
                } catch (IllegalArgumentException e) {
                    throw new VerifyException(e);
                }
                latch.countDown();
            }
        }, new Metadata());
        call.sendMessage(HelloRequest.newBuilder().build());
        call.halfClose();

        if (!Uninterruptibles.awaitUninterruptibly(latch, 1, TimeUnit.SECONDS)) {
            throw new RuntimeException("timeout!");
        }
    }

    static void verifyErrorReply(Throwable t) {
        Status status = Status.fromThrowable(t);
        Metadata trailers = Status.trailersFromThrowable(t);
        Verify.verify(status.getCode() == Status.Code.INTERNAL);
        Verify.verify(trailers.containsKey(DEBUG_INFO_KEY));
        Verify.verify(status.getDescription().equals(DEBUG_DESC));
        try {
            Verify.verify(trailers.get(DEBUG_INFO_KEY).equals(DEBUG_INFO));
        } catch (IllegalArgumentException e) {
            throw new VerifyException(e);
        }
    }

}
